package br.org.bertol.mestrado.engine.optimisation.moo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

import org.apache.log4j.Logger;

import br.org.bertol.mestrado.engine.Objective;
import br.org.bertol.mestrado.engine.optimisation.entity.Particle;
import br.org.bertol.mestrado.engine.optimisation.pso.PsoRepository;

/**
 * @author contaqualquer
 * @param <T>
 *            Tipo da partícula
 */
public class AdaptativeGrid<T> {

    /***/
    private final transient ArrayList<Particle<T>>     archive;

    /***/
    private final transient ArrayList<Particle<T>>     notAllowedPareto;

    /***/
    private final transient ArrayList<Double>          gridPlacesChances;

    /***/
    private final transient ArrayList<Integer>         gridPlacesOccupiedPlaces;

    /***/
    private final transient double[]                   gridPopulation;

    /***/
    private final transient EnumMap<Objective, Double> globalLowerLimit;

    /***/
    private final transient EnumMap<Objective, Double> globalUpperLimit;

    /***/
    private final transient PsoRepository              psoRepository;

    /**
     * Objetivos do problema.
     */
    private final transient Objective[]                objectives;

    /***/
    private final transient int                        DIVISIONS;

    /***/
    private final transient int                        MAX_GRID_SIZE;

    /***/
    private final transient int                        MAX_OBJECTIVES;

    /***/
    private final transient int                        REPOSITORY_SIZE;

    /***/
    private transient float                            gridChancesSum;

    /***/
    private final int                                  SIZE_NOT_ALLOWED_REP;

    /**
     * Cria grid para algoritmos de população.
     * @param verify
     * @param divisons
     * @param repositorySize
     * @param paretoRepository
     */
    public AdaptativeGrid(final int divisons, final int repositorySize,
        final PsoRepository paretoRepository, final Objective... targets) {

        SIZE_NOT_ALLOWED_REP = 1000;

        archive = new ArrayList<Particle<T>>(0);

        notAllowedPareto = new ArrayList<Particle<T>>(SIZE_NOT_ALLOWED_REP);

        notAllowedPareto.ensureCapacity(SIZE_NOT_ALLOWED_REP);

        MAX_OBJECTIVES = targets.length;

        DIVISIONS = divisons;

        MAX_GRID_SIZE = Double.valueOf(Math.pow(DIVISIONS, MAX_OBJECTIVES))
                .intValue();

        REPOSITORY_SIZE = repositorySize;

        globalLowerLimit = new EnumMap<Objective, Double>(Objective.class);

        globalUpperLimit = new EnumMap<Objective, Double>(Objective.class);

        for (Objective objective : targets) {
            globalLowerLimit.put(objective, 0d);

            globalUpperLimit.put(objective, 0d);
        }

        // sempre mais quadrados que o n�mero q ser� c�lculado para evitar out
        // of bounds
        gridPopulation = new double[MAX_GRID_SIZE];

        gridChancesSum = 0f;

        gridPlacesChances = new ArrayList<Double>();

        gridPlacesOccupiedPlaces = new ArrayList<Integer>();

        this.psoRepository = paretoRepository;

        this.objectives = targets;

    }

    /**
     * Inicia a grid.
     * @param list
     *            Lista inicial de partículas.
     * @throws Exception
     *             Erro ao inserir
     */
    @SuppressWarnings("unchecked")
    public final void startGrid(final List<Particle<T>> list) throws Exception {

        final ArrayList<Particle<T>> start =
                new ArrayList<Particle<T>>(list);

        Collections.sort(start, new OrdenatePareto<Particle<T>>(
                new ParetoMinMinDominace()));

        final ArrayList<Particle<T>> tmp =
                new ArrayList<Particle<T>>();

        for (Particle<T> u : start) {
            tmp.add((Particle<T>) u.clone());
        }

        tmp.trimToSize();

        archive.addAll(tmp.subList(0, (tmp.size() < REPOSITORY_SIZE ? tmp
                .size() : REPOSITORY_SIZE)));

        updateGridPosition();
    }

    /**
     * @return the archive
     */
    public final ArrayList<Particle<T>> getGrid() {
        return archive;
    }

    /**
     * @return the gridPopulation
     */
    public final double[] getGridPopulation() {
        return gridPopulation;
    }

    /**
     * Recupera a partícula guia do grid.
     * @return Partícula selecionada
     * @throws CloneNotSupportedException
     *             Erro ao clonar objeto
     */
    @SuppressWarnings("unchecked")
    public final Particle<T> getUnit() throws CloneNotSupportedException {

        if (!archive.isEmpty()) {

            if (archive.size() == 1) {
                return (Particle<T>) archive.get(0).clone();
            }

            double partialChance = 0;

            int pointer = 0, gridposition = 0;

            final Random random = new Random(System.currentTimeMillis());

            final double randomChance = random.nextDouble() * gridChancesSum;

            for (pointer = 0; pointer < gridPlacesOccupiedPlaces.size()
                    && partialChance < randomChance; pointer++) {
                partialChance += gridPlacesChances.get(pointer);
                gridposition = gridPlacesOccupiedPlaces.get(pointer);
            }

            final List<Particle<T>> seletetdSquares =
                    new ArrayList<Particle<T>>();

            for (Particle<T> particle : archive) {
                if (particle.getSpacialPosition() == gridposition) {
                    seletetdSquares.add(particle);
                }
            }

            final int randPArticule = (int) Math.floor(seletetdSquares.size()
                    * random.nextDouble());

            return seletetdSquares.get(randPArticule);
        }

        return null;
    }

    /**
     * Atualiza a posição das partírculas no grid.
     */
    public final void updateGridStatus() {

        // zera todos os contadores
        gridPlacesChances.clear();

        gridPlacesChances.trimToSize();

        gridPlacesOccupiedPlaces.clear();

        gridPlacesOccupiedPlaces.trimToSize();

        gridChancesSum = 0f;

        // realiza contagem dos corpos
        for (int i = 0; i < gridPopulation.length; i++) {

            if (gridPopulation[i] > 0d) {
                gridPlacesChances.add(1.0 / gridPopulation[i]);
                gridPlacesOccupiedPlaces.add(i);
                gridChancesSum += 1.0 / gridPopulation[i];
            }
        }

    }

    /**
     * Realiza verificação de pareto para atualização do local best de uma
     * partícula.
     * @param particleList
     *            Lista de partículas a serem inseridas
     * @throws Exception
     *             Erro ao inserir
     */
    @SuppressWarnings("unchecked")
    public final void updateGrid(final List<Particle<T>> particleList)
            throws Exception {

        for (Particle<T> unit : particleList) {

            // verifica se o movimento da part�cula � v�lido

            // verificação para mono objetivo
            if (objectives.length == 1) {

                final Particle<T> gbest = (Particle<T>) archive.get(0).clone();

                if (unit.getHeight().get(objectives[0]).intValue() < gbest
                        .getHeight().get(objectives[0]).intValue()) {

                    Logger.getLogger(getClass())
                            .debug(
                                   "Melhor GBest encontrado.");

                    Logger.getLogger(getClass()).debug(
                                                       "GBest anterior: "
                            + gbest);

                    archive.set(0, (Particle<T>) unit.clone());

                    Logger.getLogger(getClass())
                            .debug(
                                   "GBest    atual: " + archive.get(0));
                }
            } else {

                final int paretoDominante = psoRepository
                        .verifyDominance(unit);

                if (paretoDominante != ParetoDominace.RIGHT_DOMINE) {

                    // caso seja dominante
                    if (paretoDominante == ParetoDominace.LEFT_DOMINE
                            || archive.size() < REPOSITORY_SIZE) {
                        addGridUnit(unit);
                    } else {
                        // caso não tenha dominância e arquivo esteja full
                        // usar uma posição do hiper cubo menos populada
                        // substitui unidade

                        replaceGridUnit(unit);
                    }

                    archive.trimToSize();
                }
            }

            /*
             * if(!notAllowedPareto.contains(unit)) // adiciona unida
             * notAllowedPareto.add((U) unit.clone());
             */
        }
        notAllowedPareto.trimToSize();
    }

    /**
     * Adiciona no grid unidade.
     * @param unit
     *            Nova unidade
     * @throws Exception
     *             Erro ao inserir na grid
     */
    @SuppressWarnings("unchecked")
    private void addGridUnit(final Particle<T> unit) throws Exception {

        Logger.getLogger(getClass()).debug("#####Início da operação.#####");

        final Particle<T> newUnit = (Particle<T>) unit.clone();

        // verifyDominance(new_unit);

        final int square = findGridUnitLocation(newUnit.getHeight());

        archive.add(newUnit);

        newUnit.setSpacialPosition(square - 1);

        // caso diferente de zero, marca
        if (square != 0) {
            gridPopulation[newUnit.getSpacialPosition()]++;
            // sen�o atualiza todas as posi��es
        } else {
            updateGridPosition();
        }

        archive.trimToSize();

        Logger.getLogger(getClass())
                .info(
                      "Nova unidade adicionada. Sum�rio da opera��o ---- : "
                + newUnit);

        Logger.getLogger(getClass())
                .debug(
                       "Ocupa��o atual do reposit�rio ------------------ : "
                + archive.size());

        Logger.getLogger(getClass()).debug("#####Fim da opera��o.#####");

    }

    /**
     * Substitui um unidade do grid.
     * @param unit
     *            Nova unidade
     * @throws Exception
     *             Erro ao substituir.
     */
    @SuppressWarnings("unchecked")
    private void replaceGridUnit(final Particle<T> unit) throws Exception {

        Logger.getLogger(getClass()).debug("#####In�cio da opera��o.#####");

        Logger.getLogger(getClass()).debug("Substituindo part�cula.");

        // realiza verifica��o no candidato

        // final U new_unit = applyILS(unit);

        final Particle<T> newUnit = (Particle<T>) unit.clone();

        // verifyDominance(new_unit);

        final int square = findGridUnitLocation(newUnit.getHeight());

        final int maxPopulation = searchMaxPopulated();

        Logger.getLogger(getClass())
                .debug(
                       "Quadrante mais ocupada: " + maxPopulation
                + ". População: "
                + gridPopulation[maxPopulation]);

        // caso n�o tenha lugar no grid
        // remove uma part�cula da posi��o mais populosa
        // e adiciona a nova l�
        if (square == 0) {

            Particle<T> erased = null;

            for (Particle<T> p : archive) {
                if (p.getSpacialPosition() == maxPopulation) {
                    erased = p;
                    break;
                }
            }
            // apaga item
            archive.remove(erased);

            Logger.getLogger(getClass())
                    .debug(
                           "Partícula removida: Suá�rio da operação ------ : "
                    + erased);

            // adiciona novo candidato
            archive.add(newUnit);
            // atualiza grid
            updateGridPosition();

            Logger.getLogger(getClass())
                    .debug(
                           "Ocupação atual do reposit�rio ---------------- : "
                    + archive.size());
        } else if (gridPopulation[square - 1] < gridPopulation[maxPopulation]) {
            // caso a posição da escolhida da partícula tenha
            // uma população menor, adiciona a partícula nova
            // e transfere uma de outra posição, senão descarta

            Particle<T> erased = null;

            for (Particle<T> p : archive) {
                if (p.getSpacialPosition() == maxPopulation) {
                    erased = p;
                    break;
                }
            }
            // apaga item
            archive.remove(erased);
            gridPopulation[erased.getSpacialPosition()]--;
            Logger
                    .getLogger(getClass())
                    .debug(
                           "Popula��o do quadrante "
                    + erased.getSpacialPosition()
                    + " : "
                    + gridPopulation[erased.getSpacialPosition()]);

            Logger.getLogger(getClass())
                    .debug(
                           "Part�cula removida: Sum�rio da opera��o ---- : "
                    + erased);

            // adiciona novo candidato
            archive.add(newUnit);
            newUnit.setSpacialPosition(square - 1);
            gridPopulation[newUnit.getSpacialPosition()]++;
            Logger.getLogger(getClass())
                    .debug(
                           "População do quadrante "
                    + newUnit.getSpacialPosition()
                    + " : "
                    + gridPopulation[newUnit
                    .getSpacialPosition()]);

            Logger.getLogger(getClass())
                    .info(
                          "Nova unidade adicionada. Sumário da operação -- : "
                    + newUnit);

            Logger.getLogger(getClass())
                    .debug(
                           "Ocupa��o atual do reposit�rio ---------------- : "
                    + archive.size());

        } else {
            Logger.getLogger(getClass())
                    .debug(
                           "Partícula descartada ----------------------- : "
                    + newUnit);
        }

        Logger.getLogger(getClass()).debug("#####Fim da opera��o.#####");
    }

    /**
     * Atualiza as posições das unidade no grid.
     */
    private void updateGridPosition() {

        final EnumMap<Objective, Number> localLimInf =
                new EnumMap<Objective, Number>(Objective.class);

        final EnumMap<Objective, Number> localSupLim =
                new EnumMap<Objective, Number>(Objective.class);

        // inicia arrays auxiliares
        for (Objective objective : objectives) {
            localLimInf.put(objective, Double.MAX_VALUE);

            localSupLim.put(objective, Double.MIN_VALUE);
        }

        // procura o maior e menor f(x) dentro os elementos do arquivo
        for (Objective objective : objectives) {
            for (Particle<T> ndPartice : archive) {
                // menor objetivo vai para offset
                if (ndPartice.getHeight().get(objective).intValue() < localLimInf
                        .get(objective).doubleValue()) {

                    localLimInf.put(objective, ndPartice.getHeight()
                            .get(objective).doubleValue());
                }
                // maior objetivo vai para largest
                if (ndPartice.getHeight().get(objective).intValue() > localSupLim
                        .get(objective).doubleValue()) {

                    localSupLim.put(objective, ndPartice.getHeight()
                            .get(objective).doubleValue());
                }
            }
        }

        for (Objective objective : objectives) {
            globalLowerLimit
                    .put(
                         objective,
                         (localSupLim.get(objective).doubleValue() - localLimInf
                                 .get(objective).doubleValue())
                                 / (DIVISIONS - 1));

            globalUpperLimit.put(objective, localLimInf.get(objective)
                    .doubleValue()
                    - (globalLowerLimit.get(objective).doubleValue() / 2.0));
        }

        for (int square = 0; square < MAX_GRID_SIZE; square++) {
            gridPopulation[square] = 0;
        }

        // reposiciona todas as part�culas
        for (Particle<T> paretoFrontItem : archive) {
            int square = findGridUnitLocation(paretoFrontItem.getHeight()) - 1;
            paretoFrontItem.setSpacialPosition(square);
            gridPopulation[paretoFrontItem.getSpacialPosition()]++;
        }
    }

    /**
     * Encontra a posição espacial de uma determinada part�cula de acordo com os
     * valores das funções objetivos.
     * @param objectiveMap
     *            Lista de Objetivos
     * @return Posição da unidade no grid
     */
    private int findGridUnitLocation(
            final EnumMap<Objective, Number> objectiveMap) {

        // localização de partícula
        int location = 0, dim = MAX_OBJECTIVES - 1;

        // verifica se as solução para cada objetivo pesquisado est�o
        // dentro do grid
        for (final Iterator<Objective> iterator = objectiveMap.keySet()
                .iterator(); iterator
                .hasNext(); dim--) {

            final Objective objective = iterator.next();

            final int temp = (int) (Math.floor((objectiveMap.get(objective)
                    .doubleValue() - globalUpperLimit.get(objective)
                    .doubleValue())
                    / globalUpperLimit.get(objective).doubleValue()));

            if (temp > DIVISIONS || temp < 0) {
                return 0;
            }

            location += Math.floor(temp * Math.pow(DIVISIONS, dim));

        }
        return location + 1;
    }

    /**
     * Busca hipercubo com maior densidade populacional.
     * @return Posição do hipercubo em array
     */
    private int searchMaxPopulated() {

        int i, fmax = 0;

        for (i = 1; i < MAX_GRID_SIZE; i++) {
            if (gridPopulation[fmax] < gridPopulation[i]) {
                fmax = i;
            }
        }
        return fmax;
    }
}
